/** * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the * License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by * applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ package org.apache.camel.component.jsch; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; import java.util.Scanner; import org.apache.camel.Exchange; import org.apache.camel.InvalidPayloadException; import org.apache.camel.component.file.GenericFileEndpoint; import org.apache.camel.component.file.GenericFileOperationFailedException; import org.apache.camel.component.file.remote.RemoteFile; import org.apache.camel.component.file.remote.RemoteFileConfiguration; import org.apache.camel.component.file.remote.RemoteFileOperations; import org.apache.camel.util.IOHelper; import org.apache.camel.util.ObjectHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.jcraft.jsch.Channel; import com.jcraft.jsch.ChannelExec; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; import com.jcraft.jsch.UIKeyboardInteractive; import com.jcraft.jsch.UserInfo; /** * SCP remote file operations */ public class ScpOperations implements RemoteFileOperations<ScpFile> { private static final String DEFAULT_KNOWN_HOSTS = "META-INF/.ssh/known_hosts"; private static final Logger LOG = LoggerFactory.getLogger(ScpOperations.class); private ScpEndpoint endpoint; private Session session; private ChannelExec channel; @Override public void setEndpoint(GenericFileEndpoint<ScpFile> endpoint) { this.endpoint = (ScpEndpoint) endpoint; } @Override public boolean deleteFile(String name) throws GenericFileOperationFailedException { throw new GenericFileOperationFailedException("Operation 'delete' not supported by the scp: protocol"); } @Override public boolean existsFile(String name) throws GenericFileOperationFailedException { return false; } @Override public boolean renameFile(String from, String to) throws GenericFileOperationFailedException { throw new GenericFileOperationFailedException("Operation 'rename' not supported by the scp: protocol"); } @Override public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException { return true; } @SuppressWarnings("unchecked") @Override public boolean retrieveFile(String name, Exchange exchange) throws GenericFileOperationFailedException { OutputStream outputStream = null; RemoteFile<ScpFile> remoteFile = exchange.getIn().getBody(RemoteFile.class); try { // exec 'scp -f rfile' remotely String command = "scp -f " + remoteFile.getAbsoluteFilePath(); Channel channel = session.openChannel("exec"); ((ChannelExec) channel).setCommand(command); // get I/O streams for remote scp OutputStream out = channel.getOutputStream(); InputStream inputStream = channel.getInputStream(); channel.connect(); byte[] buf = new byte[1024]; // send '\0' buf[0] = 0; out.write(buf, 0, 1); out.flush(); while (true) { int c = checkAck(inputStream); if (c != 'C') { break; } // read '0644 ' inputStream.read(buf, 0, 5); long filesize = 0L; while (true) { if (inputStream.read(buf, 0, 1) < 0) { // error break; } if (buf[0] == ' ') { break; } filesize = filesize * 10L + buf[0] - '0'; } for (int i = 0;; i++) { inputStream.read(buf, i, 1); if (buf[i] == (byte) 0x0a) { break; } } // send '\0' buf[0] = 0; out.write(buf, 0, 1); out.flush(); // read content outputStream = new ByteArrayOutputStream(); int foo; while (true) { if (buf.length < filesize) { foo = buf.length; } else { foo = (int) filesize; } foo = inputStream.read(buf, 0, foo); if (foo < 0) { // error break; } outputStream.write(buf, 0, foo); filesize -= foo; if (filesize == 0L) { break; } } if (checkAck(inputStream) != 0) { LOG.warn("Issues while retrieving the file. Will try again in the next poll."); return false; } // send '\0' buf[0] = 0; out.write(buf, 0, 1); out.flush(); exchange.getIn().setBody(outputStream); return true; } } catch (Exception e) { LOG.warn("Issues while retrieving the file. Will try again in the next poll. Exception: ", e); } return false; } private static int checkAck(InputStream in) throws IOException { int b = in.read(); // b may be 0 for success, // 1 for error, // 2 for fatal error, // -1 if (b == 0) { return b; } if (b == -1) { return b; } if (b == 1 || b == 2) { StringBuffer sb = new StringBuffer(); int c; do { c = in.read(); sb.append((char) c); } while (c != '\n'); if (b == 1) { // error System.out.print(sb.toString()); } if (b == 2) { // fatal error System.out.print(sb.toString()); } } return b; } @Override public void releaseRetreivedFileResources(Exchange exchange) throws GenericFileOperationFailedException { OutputStream outputStream = exchange.getIn().getBody(OutputStream.class); if(outputStream != null) { try { outputStream.close(); } catch(Exception e) { LOG.trace("Exception caught while closing output stream: ", e); } } } @Override public boolean storeFile(String name, Exchange exchange) throws GenericFileOperationFailedException { ObjectHelper.notNull(session, "session"); ScpConfiguration cfg = endpoint.getConfiguration(); int timeout = cfg.getConnectTimeout(); if (LOG.isTraceEnabled()) { LOG.trace("Opening channel to {} with {} timeout...", cfg.remoteServerInformation(), timeout > 0 ? (Integer.toString(timeout) + " ms") : "no"); } String file = getRemoteFile(name, cfg); InputStream is = null; if (exchange.getIn().getBody() == null) { // Do an explicit test for a null body and decide what to do if (endpoint.isAllowNullBody()) { LOG.trace("Writing empty file."); is = new ByteArrayInputStream(new byte[] {}); } else { throw new GenericFileOperationFailedException("Cannot write null body to file: " + name); } } try { channel = (ChannelExec) session.openChannel("exec"); channel.setCommand(getScpCommand(cfg, file)); channel.connect(timeout); LOG.trace("Channel connected to {}", cfg.remoteServerInformation()); try { if (is == null) { is = exchange.getIn().getMandatoryBody(InputStream.class); } write(channel, file, is, cfg); } catch (InvalidPayloadException e) { throw new GenericFileOperationFailedException("Cannot store file: " + name, e); } catch (IOException e) { throw new GenericFileOperationFailedException("Failed to write file " + file, e); } finally { // must close stream after usage IOHelper.close(is); } } catch (JSchException e) { throw new GenericFileOperationFailedException("Failed to write file " + file, e); } finally { if (channel != null) { LOG.trace("Disconnecting 'exec' scp channel"); channel.disconnect(); channel = null; LOG.trace("Channel disconnected from {}", cfg.remoteServerInformation()); } } return true; } @Override public String getCurrentDirectory() throws GenericFileOperationFailedException { return endpoint.getConfiguration().getDirectory(); } @Override public void changeCurrentDirectory(String path) throws GenericFileOperationFailedException { throw new GenericFileOperationFailedException("Operation 'cd " + path + "' not supported by the scp: protocol"); } @Override public void changeToParentDirectory() throws GenericFileOperationFailedException { throw new GenericFileOperationFailedException("Operation 'cd ..' not supported by the scp: protocol"); } @Override public List<ScpFile> listFiles() throws GenericFileOperationFailedException { System.out.println("listFiles"); throw new GenericFileOperationFailedException("Operation 'ls' not supported by the scp: protocol"); } @Override public List<ScpFile> listFiles(String path) throws GenericFileOperationFailedException { List<ScpFile> files = new ArrayList<ScpFile>(); StringBuffer sb = new StringBuffer(); try { String command = "ls -als " + path; Channel channel = session.openChannel("exec"); ((ChannelExec) channel).setCommand(command); ((ChannelExec) channel).setErrStream(System.err); InputStream in = channel.getInputStream(); channel.connect(); byte[] tmp = new byte[1024]; while (true) { while (in.available() > 0) { int i = in.read(tmp, 0, 1024); if (i < 0) { break; } sb.append(new String(tmp, 0, i)); } if (channel.isClosed()) { if (in.available() > 0) { continue; } break; } try { Thread.sleep(1000); } catch (Exception ee) {} } channel.disconnect(); } catch (Exception e) { System.err.println(e); } Scanner scanner = new Scanner(sb.toString()); if (scanner.hasNextLine()) { scanner.nextLine(); scanner.nextLine(); scanner.nextLine(); while (scanner.hasNextLine()) { ScpFile scpFile = getScpFilename(scanner.nextLine(), path); if (scpFile != null) { files.add(scpFile); } } } return files; } private ScpFile getScpFilename(String nextLine, String path) { if (nextLine == null || nextLine.equals("")) { return null; } ScpFile scpFile = new ScpFile(); String[] pieces = nextLine.split(" +"); if (!pieces[1].startsWith("d")) { int owner = getPermission(pieces[1].substring(1, 4)); int group = getPermission(pieces[1].substring(4, 7)); int other = getPermission(pieces[1].substring(7, 10)); int attr = owner * 100 + group * 10 + other; scpFile.setAttrs(attr); scpFile.setName(pieces[9]); scpFile.setParent(path); scpFile.setLength(Integer.parseInt(pieces[5])); scpFile.setDirectory(false); return scpFile; } else { return null; } } private int getPermission(String permission) { int permissionVal = 0; if (permission.contains("r")) { permissionVal += +4; } if (permission.contains("w")) { permissionVal += +2; } if (permission.contains("x") || permission.contains("t")) { permissionVal += +1; } return permissionVal; } @Override public boolean connect(RemoteFileConfiguration configuration) throws GenericFileOperationFailedException { if (!isConnected()) { session = createSession(configuration instanceof ScpConfiguration ? (ScpConfiguration) configuration : null); // TODO: deal with reconnection attempts if (!isConnected()) { session = null; throw new GenericFileOperationFailedException("Failed to connect to " + configuration.remoteServerInformation()); } } return true; } @Override public boolean isConnected() throws GenericFileOperationFailedException { return session != null && session.isConnected(); } @Override public void disconnect() throws GenericFileOperationFailedException { if (isConnected()) { session.disconnect(); } session = null; } @Override public boolean sendNoop() throws GenericFileOperationFailedException { return true; } @Override public boolean sendSiteCommand(String command) throws GenericFileOperationFailedException { return true; } private Session createSession(ScpConfiguration config) { ObjectHelper.notNull(config, "ScpConfiguration"); try { final JSch jsch = new JSch(); // get from configuration if (ObjectHelper.isNotEmpty(config.getCiphers())) { LOG.debug("Using ciphers: {}", config.getCiphers()); Hashtable<String, String> ciphers = new Hashtable<String, String>(); ciphers.put("cipher.s2c", config.getCiphers()); ciphers.put("cipher.c2s", config.getCiphers()); JSch.setConfig(ciphers); } if (ObjectHelper.isNotEmpty(config.getPrivateKeyFile())) { LOG.debug("Using private keyfile: {}", config.getPrivateKeyFile()); String pkfp = config.getPrivateKeyFilePassphrase(); jsch.addIdentity(config.getPrivateKeyFile(), ObjectHelper.isNotEmpty(pkfp) ? pkfp : null); } String knownHostsFile = config.getKnownHostsFile(); jsch.setKnownHosts(ObjectHelper.isEmpty(knownHostsFile) ? DEFAULT_KNOWN_HOSTS : knownHostsFile); session = jsch.getSession(config.getUsername(), config.getHost(), config.getPort()); session.setTimeout(config.getTimeout()); session.setUserInfo(new SessionUserInfo(config)); if (ObjectHelper.isNotEmpty(config.getStrictHostKeyChecking())) { LOG.debug("Using StrickHostKeyChecking: {}", config.getStrictHostKeyChecking()); session.setConfig("StrictHostKeyChecking", config.getStrictHostKeyChecking()); } else { LOG.debug("Using StrickHostKeyChecking: {}", "no"); session.setConfig("StrictHostKeyChecking", "no"); } int timeout = config.getConnectTimeout(); LOG.debug("Connecting to {} with {} timeout...", config.remoteServerInformation(), timeout > 0 ? (Integer.toString(timeout) + " ms") : "no"); if (timeout > 0) { session.connect(timeout); } else { session.connect(); } } catch (JSchException e) { session = null; LOG.warn("Could not create ssh session for " + config.remoteServerInformation(), e); } return session; } private void write(ChannelExec c, String name, InputStream data, ScpConfiguration cfg) throws IOException { OutputStream os = c.getOutputStream(); InputStream is = c.getInputStream(); try { writeFile(name, data, os, is, cfg); } finally { IOHelper.close(is, os); } } private void writeFile(String filename, InputStream data, OutputStream os, InputStream is, ScpConfiguration cfg) throws IOException { final int lineFeed = '\n'; String bytes; int pos = filename.indexOf('/'); if (pos >= 0) { // write to child directory String dir = filename.substring(0, pos); bytes = "D0775 0 " + dir; LOG.trace("[scp:sink] {}", bytes); os.write(bytes.getBytes()); os.write(lineFeed); os.flush(); readAck(is, false); writeFile(filename.substring(pos + 1), data, os, is, cfg); bytes = "E"; LOG.trace("[scp:sink] {}", bytes); os.write(bytes.getBytes()); os.write(lineFeed); os.flush(); readAck(is, false); } else { int count = 0; int read; int size = endpoint.getBufferSize(); byte[] reply = new byte[size]; // figure out the stream size as we need to pass it in the header BufferedInputStream buffer = new BufferedInputStream(data, size); try { buffer.mark(Integer.MAX_VALUE); while ((read = buffer.read(reply)) != -1) { count += read; } // send the header bytes = "C0" + cfg.getChmod() + " " + count + " " + filename; LOG.trace("[scp:sink] {}", bytes); os.write(bytes.getBytes()); os.write(lineFeed); os.flush(); readAck(is, false); // now send the stream buffer.reset(); while ((read = buffer.read(reply)) != -1) { os.write(reply, 0, read); } writeAck(os); readAck(is, false); } finally { IOHelper.close(buffer); } } } private void writeAck(OutputStream os) throws IOException { os.write(0); os.flush(); } private int readAck(InputStream is, boolean failOnEof) throws IOException { String message; int answer = is.read(); switch (answer) { case -1: if (failOnEof) { message = "[scp] Unexpected end of stream"; throw new EOFException(message); } break; case 1: message = "[scp] WARN " + readLine(is); LOG.warn(message); break; case 2: message = "[scp] NACK " + readLine(is); throw new IOException(message); default: // case 0: break; } return answer; } @SuppressWarnings("resource") private String readLine(InputStream is) throws IOException { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); try { int c; do { c = is.read(); if (c == '\n') { return bytes.toString(); } bytes.write(c); } while (c != -1); } finally { IOHelper.close(bytes); } String message = "[scp] Unexpected end of stream"; throw new IOException(message); } private static String getRemoteTarget(ScpConfiguration config) { // use current dir (".") if target directory not specified in uri return config.getDirectory().isEmpty() ? "." : config.getDirectory(); } private static String getRemoteFile(String name, ScpConfiguration config) { String dir = config.getDirectory(); dir = dir.endsWith("/") ? dir : dir + "/"; return name.startsWith(dir) ? name.substring(dir.length()) : name; } private static boolean isRecursiveScp(String name) { return name.indexOf('/') > 0; } private static String getScpCommand(ScpConfiguration config, String name) { StringBuilder cmd = new StringBuilder(); cmd.append("scp "); // TODO: need config for scp *-p* (preserves modification times, access times, and modes from the original file) // String command="scp " + (ptimestamp ? "-p " : "") + "-t " + configuration.getDirectory(); // TODO: refactor to use generic command cmd.append(isRecursiveScp(name) ? "-r " : ""); cmd.append("-t "); cmd.append(getRemoteTarget(config)); return cmd.toString(); } protected static final class SessionUserInfo implements UserInfo, UIKeyboardInteractive { private final ScpConfiguration config; public SessionUserInfo(ScpConfiguration config) { ObjectHelper.notNull(config, "config"); this.config = config; } @Override public String getPassphrase() { LOG.warn("Private Key authentication not supported"); return null; } @Override public String getPassword() { LOG.debug("Providing password for ssh authentication of user '{}'", config.getUsername()); return config.getPassword(); } @Override public boolean promptPassword(String message) { LOG.debug(message); return true; } @Override public boolean promptPassphrase(String message) { LOG.debug(message); return true; } @Override public boolean promptYesNo(String message) { LOG.debug(message); return false; } @Override public void showMessage(String message) { LOG.debug(message); } @Override public String[] promptKeyboardInteractive(String destination, String name, String instruction, String[] prompt, boolean[] echo) { LOG.debug(instruction); // Called for either SSH_MSG_USERAUTH_INFO_REQUEST or SSH_MSG_USERAUTH_PASSWD_CHANGEREQ // The most secure choice (especially for the second case) is to return null return null; } } }